package xyz.huanggy.minio.upload.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Part;
import lombok.SneakyThrows;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import xyz.huanggy.minio.upload.configuration.StringRedisTemplate;
import xyz.huanggy.minio.upload.mapper.SysFileInfoMapper;
import xyz.huanggy.minio.upload.minio.CustomMinioClient;
import xyz.huanggy.minio.upload.minio.MinioProperties;
import xyz.huanggy.minio.upload.model.dto.TaskInfoDTO;
import xyz.huanggy.minio.upload.model.dto.TaskRecordDTO;
import xyz.huanggy.minio.upload.model.entity.SysFileInfo;
import xyz.huanggy.minio.upload.model.param.InitTaskParam;
import xyz.huanggy.minio.upload.service.SysFileInfoService;

import javax.annotation.Resource;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 分片上传-分片任务记录(SysUploadTask)表服务实现类
 *
 * @since 2022-08-22 17:47:31
 */
@Service("sysUploadTaskService")
public class SysFileInfoServiceImpl implements SysFileInfoService {


    @Resource
    private MinioProperties minioProperties;

    @Resource
    private CustomMinioClient customMinioClient;
    @Resource
    private SysFileInfoMapper sysFileInfoMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @SneakyThrows
    @Override
    public List<SysFileInfo> sysFileList() {
        List<SysFileInfo> fileInfos = sysFileInfoMapper.selectList(new LambdaQueryWrapper<SysFileInfo>().orderByDesc(SysFileInfo::getId));
        for (SysFileInfo fileInfo : fileInfos) {
            GetPresignedObjectUrlArgs.Builder builder = GetPresignedObjectUrlArgs.builder()
                    .bucket(fileInfo.getBucketName())
                    .object(fileInfo.getUrl())
                    .method(Method.GET)
                    .expiry(1, TimeUnit.DAYS);
            String url = customMinioClient.getPresignedObjectUrl(builder.build());
            fileInfo.setUrl(url);
        }

        return fileInfos;
    }

    @Override
    public TaskInfoDTO queryProgress(String md5) {

        SysFileInfo task = getValidTask(md5, false);
        if (task == null) {
            return null;
        }


        TaskInfoDTO result = new TaskInfoDTO().setFinished(true).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(task.getBucketName(), task.getUrl()));

        boolean doesObjectExist = doesObjectExist(task.getBucketName(), task.getUrl());
        if (!doesObjectExist) {
            // 未上传完，返回已上传的分片
            ListPartsResponse partResult = customMinioClient.listParts(task.getBucketName(), null, task.getUrl(), task.getChunkNum(), 0, task.getUploadId(), null, null);
            result.setFinished(false).getTaskRecord().setExitPartList(partResult.result().partList());
        }
        return result;
    }

    @Override
    public TaskInfoDTO initTask(InitTaskParam param) {

        Date currentDate = new Date();
        String bucketName = minioProperties.getBucket();
        String fileName = param.getFileName();
        String fileType = fileName.substring(fileName.lastIndexOf(".") + 1);
        String key = StrUtil.format("{}/{}.{}", DateUtil.format(currentDate, "YYYY-MM-dd"), IdUtil.randomUUID(), fileType);


        // 查找是否存在具有相同MD5码的文件
        List<SysFileInfo> fileEntityList = sysFileInfoMapper.selectList(new LambdaQueryWrapper<SysFileInfo>().eq(SysFileInfo::getMd5Code, param.getMd5()));
        if (CollectionUtils.isNotEmpty(fileEntityList)) {
            SysFileInfo existFile = fileEntityList.get(0);
            SysFileInfo newFile = new SysFileInfo();
            newFile.setName(fileName)
                    .setByteSize(param.getTotalSize())
                    .setFileType(fileType)
                    .setUrl(existFile.getUrl())
                    .setUploadId(existFile.getUploadId())
                    .setMd5Code(param.getMd5())
                    .setBucketName(existFile.getBucketName())
                    .setBusinessCode(existFile.getBusinessCode())
                    .setChunkNum(existFile.getChunkNum())
                    .setChunkSize(existFile.getChunkSize())
                    .setSubBusinessCode(existFile.getSubBusinessCode());
            sysFileInfoMapper.insert(newFile);

            return new TaskInfoDTO().setFinished(true).setPath(getPath(newFile.getBucketName(), newFile.getUrl()));
        }

        // 获取上传任务，如果存在有效任务，则返回任务信息
        SysFileInfo task = getValidTask(param.getMd5(), false);
        if (task != null) {
            TaskInfoDTO result = new TaskInfoDTO().setFinished(true).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(task.getBucketName(), task.getUrl()));
            boolean doesObjectExist = doesObjectExist(task.getBucketName(), task.getUrl());
            // 检查对象是否已存在
            if (!doesObjectExist) {
                // 未上传完，返回已上传的分片，前端进行判断，已上传的分片不再上传
                ListPartsResponse partResult = customMinioClient.listParts(task.getBucketName(), null, task.getUrl(), task.getChunkNum(), 0, task.getUploadId(), null, null);
                result.setFinished(false).getTaskRecord().setExitPartList(partResult.result().partList());
            }
            return result;
        }

        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString());

        String uploadId = customMinioClient.createMultipartUpload(bucketName, null, key, headers, null).result().uploadId();

        String redisKey = getRedisKey(param.getMd5());


        task = new SysFileInfo();
        int chunkNum = (int) Math.ceil(param.getTotalSize() * 1.0 / param.getChunkSize());
        task.setBucketName(bucketName)
                .setUrl(key)
                .setChunkNum(chunkNum)
                .setChunkSize(param.getChunkSize())
                .setByteSize(param.getTotalSize())
                .setMd5Code(param.getMd5())
                .setFileType(fileType)
                .setName(fileName)
                .setUrl(key)
                .setBucketName(bucketName)
                .setUploadId(uploadId);

        //在20小时内，可以重复使用之前未成功上传的分片文件进行分片上传（预签名url有效期是24小时，这里设置key是20小时，需要考虑上传时间，避免一些极端情况）
        stringRedisTemplate.opsForValueSet(redisKey, JSONObject.toJSONString(task), 20, TimeUnit.HOURS);
        return new TaskInfoDTO().setFinished(false).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(bucketName, key));
    }

    public SysFileInfo getValidTask(String md5, boolean isValid) {
        String redisKey = getRedisKey(md5);
        SysFileInfo task = JSONObject.parseObject(stringRedisTemplate.opsForValueGet(redisKey), SysFileInfo.class);
        if (task == null && isValid) {
            throw new RuntimeException("分片任务不存在");
        }
        return task;
    }

    private String getRedisKey(String md5) {
        return String.format("multipart_uploadId:%s", md5);
    }


    @Override
    public String getPath(String bucket, String objectKey) {
        return StrUtil.format("{}/{}/{}", minioProperties.getEndpoint(), bucket, objectKey);
    }


    /**
     * 判断MinIO桶中的对象是否存在
     *
     * @param bucketName MinIO桶的名称
     * @param objectName 存储对象的名称
     * @return 文件是否存在
     */
    public boolean doesObjectExist(String bucketName, String objectName) {
        boolean exist = true;
        try {
            customMinioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            exist = false;
        }
        return exist;
    }

    @Override
    public String genPreSignUploadUrl(String bucket, String objectKey, Map<String, String> params) {

        //链接有效期是1天
        GetPresignedObjectUrlArgs.Builder builder = GetPresignedObjectUrlArgs.builder()
                .bucket(bucket)
                .object(objectKey)
                .method(Method.PUT)
                .extraQueryParams(params)
                .expiry(1, TimeUnit.DAYS);

        try {
            return customMinioClient.getPresignedObjectUrl(builder.build());
        } catch (Exception e) {
            throw new RuntimeException("获取预签名url失败");
        }
    }

    @Override
    public void merge(String md5) {
        SysFileInfo task = getValidTask(md5, true);

        ListPartsResponse partResult = customMinioClient.listParts(task.getBucketName(), null, task.getUrl(), task.getChunkNum(), 0, task.getUploadId(), null, null);

        Part[] parts = partResult.result().partList().toArray(new Part[]{});

        if (!Objects.equals(task.getChunkNum(), parts.length)) {
            // 已上传分块数量与记录中的数量不对应，不能合并分块
            throw new RuntimeException("分片缺失，请重新上传");
        }
        customMinioClient.completeMultipartUpload(task.getBucketName(), null, task.getUrl(), task.getUploadId(), parts, null, null);

        String redisKey = getRedisKey(md5);
        stringRedisTemplate.opsForValueDelete(redisKey);


        sysFileInfoMapper.insert(task);
    }

    @Override
    public String genPreSignUploadUrl(String md5, Integer partNumber) {
        SysFileInfo task = getValidTask(md5, true);
        Map<String, String> params = new HashMap<>();
        params.put("partNumber", partNumber.toString());
        params.put("uploadId", task.getUploadId());
        return genPreSignUploadUrl(task.getBucketName(), task.getUrl(), params);
    }

    @SneakyThrows
    @Override
    public ResponseEntity<InputStreamResource> downloadFile(Long id) {

        SysFileInfo sysFileInfo = sysFileInfoMapper.selectById(id);

        // 从 MinIO 获取文件的输入流
        InputStream fileStream = customMinioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(sysFileInfo.getBucketName())
                        .object(sysFileInfo.getUrl())
                        .build()
        );

        // 对文件名进行 URL 编码，防止中文乱码
        String encodedFileName = URLEncoder.encode(sysFileInfo.getName(), StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");

        // 设置响应头，表示这是一个文件下载
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encodedFileName);
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);

        // 返回文件流
        return ResponseEntity.ok()
                .headers(headers)
                .body(new InputStreamResource(fileStream));
    }

}
